指向函数的指针

  对于一个函数只能做两件事:调用它,或者取得它的地址。通过取一个函数的地址而得到的指针,可以在后面用于调用这个函数。例如,

    void error(string s) { /* ... */ }
    void (*efct)(string);            // 指向函数的指针

    void f()
    {
        efct = &error;               // efct指向error
        efct("error");               // 通过efct调用error
    }

编译器知道efct是一个指针,并会去调用被指的函数。这也就是说,可以不写从指针得到函数的间接运算 *。与此类似,取得函数地址的 & 也可以不写:

    void (*f1)(string) = &error;     // ok
    void (*f2)(string) = error;      // 也可以:与&error意思一样

    void g()
    {
        f1("Vasa");                  // ok
        (*f1)("Mary Rose");          // 也可以
    }

在指向函数的指针的声明中也需要给出参数类型,就像函数声明一样。在指针赋值时,完整的函数类型必须完全匹配。例如,

    void (*pf)(string);               // 指向void(string)
    void f1(string);                  // void(string)
    int f2(string);                   // int(string)
    void f3(int*);                    // void(int*)

    void f()
    {
        pf = &f1;                     // ok
        pf = &f2;                     // 错误❌:返回类型不对
        pf = &f3;                     // 错误❌:参数类型不对

        pf("Hera");                   // ok
        pf(1);                        // 错误❌:参数类型不对

        int i = pf("Zeus");           // 错误❌:void赋值给int
    }

无论直接调用函数或通过某个指针去调用函数,有关参数传递的规则都完全相同。

  人们常常为了方便而为指向函数的指针类型定义一个名字,这样可以避免到处去写意义不太明显的语法形式。下面是来自UNIX系统头文件的一个例子:

    typedef void (*SIG_TYP)(int);        // 取自<signal.h>
    typedef void (*SIG_ARG_TYP)(int);
    SIG_TYP signal(int, SIG_ARG_TYP);

指向函数的指针的数组常常很有用。例如,我的基于鼠标的编辑器里的菜单系统,就是利用指向函数的指针的数组实现的,这些函数表示各种各样的操作。这个系统的细节不可能在这里描述,但下面是其中的基本思想:

    typedef void (*PF)();
    PF edit_ops[] = {            // 编辑操作
        &cut, &paste, &copy, &search
    };

    PF file_ops[] = {            // 文件管理
        &open, &append, &close, &write
    };

然后我们就可以定义并初始化一些指针,由它们去控制各种操作,通过关联于鼠标键的菜单去选择那些操作:

    PF* button2 = edit_ops;
    PF* button3 = file_ops;

在一个完整的实现里,定义一个菜单项需要提供更多的信息。例如,必须在某个地方保存有关需要显示的字符串的一个描述。随着系统的使用,鼠标键的意义也可能随环境而频繁变化,这种变化就可以(部分地)通过修改按键所对应的指针来实现。当用户选择一个菜单项时,例如按键2的项目3,就会执行相关的操作:

button2[2]();            // 调用按键2的第3个函数

要理解指向函数的指针的表达能力,一种方式就是试着去写这种代码而不用函数指针---也不用它们的更具良好行为的兄弟:虚函数(12.2.6节)。通过把新函数插入运算符表等方式,就可以在运行中修改这种菜单。在运行中构造出新菜单也同样非常容易。

  指向函数的指针可以用于提供一种简单形式的多态性例程,即那种可以应用于许多不同类型的对象的例程:

    typedef int (*CFT)(const void*, const void*);
    void ssort(void* base, size_t n, size_t sz, CFT cmp)
    /*
        对向量base的n个元素按照递增顺序排序,
        用由"cmp"所指的函数做比较,
        元素的大小是"sz"。

        Shell排序(Knuth, Vol 3, Pg84)
    */
    {
         for(int gap = n/2; gap > 0; gap /= 2)
             for(int i = gap; i < n; i++)
                 for(int j = i - gap; j >= 0; j-=gap) {
                     char* b = static_cast<char*>(base);    // 必须强制
                     char* pj = b + j * sz;                 // &base[j]
                     char* pjg = b + (j + gap)*sz;          // &base[j+gap]

                     if(cmp(pjg, pj) < 0) {                 // 交换base[j]与base[j + gap]:
                         for(int k = 0; k < sz; k++) {
                             char temp = pj[k];
                             pj[k] = pjg[k];
                             pjg[k] = temp;
                         }
                     }
                 }
    }

ssort()例程并不知道被排序的对象的类型,它只知道元素的个数(数组大小),每个元素的大小,以及应该去调用以完成比较的函数。这里有意将ssort()的类型选的与C标准库排序例程qsort()完全一样。实际程序可以使用qsort(),C++标准库算法sort(18.7.1节),或者其他特殊的排序例程。这种风格的代码在C里很常见,但它不是在C++里表述这个算法的最优美的方式(13.3节、13.5.2节)。

  这个排序函数可用于对下面这样的表格的排序:

    struct User {
        char* name;
        char* id;
        int dept;
    };

    User heads[] = {
        "Ritchie D.M.",    "dmr",    11271,
        "Sethi R.",        "ravi",   11272,
        "Szymanski T.G.",  "tgs",    11273,
        "Schryer N.L.",    "nls",    11274,
        "Schryer N.L.",    "nls",    11275,
        "Kernighan B.W.",  "bwk",    11276
    };

    void print_id(User* v, int n)
    {
        for(int i = 0; i < n; i++)
            cout << v[i].name << '\t' << v[i].id << '\t' << v[i].dept << '\n';
    }

为能完成排序,我们首先需要定义一个适当的比较函数。这种比较函数应该在其第一个参数小于第二个参数时返回负值,如果它们相等就返回0,否则就返回正值:

    int cmp1(const void* p, const void* q)    // 比较名字串
    {
        return strcmp(static_cast<const User*>(p)->name, static_cast<const User*>(q)->name);
    }

    int cmp2(const void* p, const void* q)    // 比较部门编号
    {
        return static_cast<const User*>(p)->dept - static_cast<const User*>(q)->dept;
    }

下面程序完成排序和打印:

    int main()
    {
        cout << "Heads in alphabetical order:\n";
        ssort(heads, 6, sizeof(User), cmpl);
        print_id(heads, 6);
        cout << '\n';

        cout << "Heads in order of department number:\n";
        ssort(heads, 6, sizeof(User), cmp2);
        print_id(heads, 6);
    }

你可以通过赋值或者初始化指向函数的指针的方式,取得一个重载函数的地址。在这种情况下,从一组重载函数中选择的工作是通过目标指针的类型实现的。例如,

    void f(int);
    int f(char);

    void (*pf1)(int) = &f;    // void f(int)
    int (*pf2)(char) = &f;    // int f(char)
    void (*pf3)(char) = &f;   // 错误❌:没有void f(char)

要通过指向函数的指针调用的函数,其参数类型和返回值类型都必须与指针的要求完全一致,在用函数对指针赋值或初始化时,没有隐含的参数或者返回值类型转换。这意味着

int cmp3(const mytype*, const mytype*);

不是ssort()的合适参数。究其原因,接受cmp3作为ssort的参数将会违反有关的保证:cmp3调用时参数的类型是mytype*(9.2.5节)。

🔚